跳到主要内容

Go 的内存模型 happens-before 偏序问题

什么是 happens-before

happens-before 是一种偏序关系,用来描述内存操作的可见性顺序。如果事件 A happens-before 事件 B,那么 A 的效果在 B 发生时必须是可见的。

核心概念

  • 可见性:一个 goroutine 的写操作何时能被另一个 goroutine 的读操作看到
  • 顺序性:操作执行的逻辑顺序,而非物理时间顺序
  • 同步:通过特定机制保证内存操作的顺序

为什么需要 happens-before

问题场景:数据竞争

// 危险代码:存在数据竞争
var a string
var done bool

func setup() {
a = "hello, world" // 写操作 1
done = true // 写操作 2
}

func doprint() {
if done { // 读操作 1
print(a) // 读操作 2 - 可能读到空字符串!
}
}

func main() {
go setup()
go doprint()
}

问题分析

Go 内存模型的核心规则

规则1:单一 goroutine 内的顺序

func singleGoroutine() {
var a, b int
a = 1 // 操作 1
b = 2 // 操作 2
c := a + b // 操作 3 - 能保证看到 a=1, b=2
}

规则2:传递性

如果 A happens-before B,B happens-before C,那么 A happens-before C。

建立 happens-before 的同步机制

Channel 操作

var c = make(chan int, 10)
var a string

func f() {
a = "hello, world" // 写操作 1
c <- 0 // 发送操作 - 写操作 2
}

func main() {
go f()
<-c // 接收操作 - 读操作 1
print(a) // 读操作 2 - 保证能看到 "hello, world"
}

Channel happens-before 规则

不同类型 Channel 的行为

无缓冲 Channel

// 无缓冲 channel:同步发送和接收
var c = make(chan int)
var a string

func f() {
a = "hello"
c <- 0 // 阻塞直到有接收者
}

func main() {
go f()
<-c // 接收操作
print(a) // 保证输出 "hello"
}

有缓冲 Channel

// 有缓冲 channel:第 k 个接收 happens-before 第 k+C 个发送完成
var c = make(chan int, 1) // 容量为 1

func worker(id int) {
c <- 1 // 获取令牌
// 临界区工作
fmt.Printf("Worker %d working\n", id)
<-c // 释放令牌
}

Mutex 互斥锁

var l sync.Mutex
var a string

func f() {
a = "hello, world"
l.Unlock() // 解锁操作
}

func main() {
l.Lock() // 加锁操作
go f()
l.Lock() // 再次加锁 - 等待 f() 解锁
print(a) // 保证看到 "hello, world"
}

Mutex happens-before 规则

sync.Once

var once sync.Once
var a string

func setup() {
a = "hello, world"
}

func doprint() {
once.Do(setup) // 保证 setup 只执行一次
print(a) // 保证看到 setup 的效果
}

func main() {
go doprint()
go doprint()
// 多个 goroutine,但 setup 只执行一次
}

Once happens-before 规则

常见的错误模式

错误的双重检查锁定

// 错误示例:可能出现问题
type Config struct {
data map[string]string
}

var (
config *Config
mu sync.Mutex
)

func GetConfig() *Config {
if config != nil { // 第一次检查,无锁
return config // 可能返回未完全初始化的对象!
}

mu.Lock()
defer mu.Unlock()

if config != nil { // 第二次检查,有锁
return config
}

config = &Config{
data: make(map[string]string),
}
return config
}

问题分析

正确做法

// 使用 sync.Once
var (
config *Config
once sync.Once
)

func GetConfig() *Config {
once.Do(func() {
config = &Config{
data: make(map[string]string),
}
})
return config
}

错误的标志位同步

// 错误示例
var a string
var done bool

func setup() {
a = "hello, world"
done = true // 编译器可能重排序!
}

func main() {
go setup()
for !done { // 可能永远为 false
}
print(a) // 可能输出空字符串
}

正确做法

// 使用 channel
var a string
var done = make(chan bool, 1)

func setup() {
a = "hello, world"
done <- true
}

func main() {
go setup()
<-done
print(a)
}

原子操作的 happens-before

import "sync/atomic"

var counter int64
var ready int32

func writer() {
// 业务逻辑
prepareData()

// 原子操作标记完成
atomic.StoreInt32(&ready, 1)
}

func reader() {
// 等待数据准备完成
for atomic.LoadInt32(&ready) == 0 {
runtime.Gosched()
}

// 使用数据
useData()
}

原子操作的 happens-before 保证

实践建议和最佳模式

数据初始化模式

// 模式1:使用 sync.Once 进行单例初始化
type Database struct {
conn *sql.DB
}

var (
db *Database
once sync.Once
)

func GetDB() *Database {
once.Do(func() {
db = &Database{
conn: createConnection(),
}
})
return db
}

// 模式2:使用 channel 进行启动同步
type Server struct {
ready chan struct{}
data map[string]interface{}
}

func NewServer() *Server {
s := &Server{
ready: make(chan struct{}),
data: make(map[string]interface{}),
}

go s.init()
return s
}

func (s *Server) init() {
// 初始化逻辑
s.loadConfig()
s.connectDB()

// 标记初始化完成
close(s.ready)
}

func (s *Server) WaitReady() {
<-s.ready
}

生产者-消费者模式

func producerConsumer() {
data := make(chan int, 10)
done := make(chan struct{})

// 生产者
go func() {
defer close(data)
for i := 0; i < 100; i++ {
data <- i
}
}()

// 消费者
go func() {
defer close(done)
for value := range data {
process(value)
}
}()

<-done // 等待消费完成
}

优雅关闭模式

type Worker struct {
quit chan struct{}
done chan struct{}
}

func (w *Worker) Start() {
go func() {
defer close(w.done)

ticker := time.NewTicker(time.Second)
defer ticker.Stop()

for {
select {
case <-ticker.C:
w.doWork()
case <-w.quit:
return // 收到关闭信号
}
}
}()
}

func (w *Worker) Stop() {
close(w.quit) // 发送关闭信号
<-w.done // 等待工作完成
}

调试和检测

使用 race detector

# 编译时启用竞态检测
go build -race main.go

# 运行时启用竞态检测
go run -race main.go

# 测试时启用竞态检测
go test -race ./...

常见竞态检测输出

==================
WARNING: DATA RACE
Write at 0x00c000110230 by goroutine 7:
main.setup()
/Users/user/main.go:8 +0x44

Previous read at 0x00c000110230 by main goroutine:
main.main()
/Users/user/main.go:13 +0x88

Goroutine 7 (running) created at:
main.main()
/Users/user/main.go:12 +0x7a
==================

总结

核心要点

  1. happens-before 是顺序关系,不是时间关系

  2. 同步机制建立 happens-before

    • Channel 发送/接收
    • Mutex 加锁/解锁
    • sync.Once
    • 原子操作
  3. 避免数据竞争

    • 使用适当的同步机制
    • 避免共享可变状态
    • 利用 race detector 检测问题
  4. 最佳实践

    • 优先使用 channel 而非共享内存
    • 合理使用 sync.Once 进行初始化
    • 使用原子操作处理简单的标志位

记住Don't communicate by sharing memory; share memory by communicating.